/* 
 * Copyright (C) 2001-2011 Jacek Sieka, arnetheduck on gmail point com
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include "stdinc.h"

#include <climits>

#include "HttpConnection.h"

#include "SettingsManager.h"
#include "version.h"

namespace dcpp {

static const string CORAL_SUFFIX = ".nyud.net";

HttpConnection::HttpConnection(MutableOutputStream* tgt, CoralizeStates aCoralState, const string& aUserAgent) :
	userAgent(aUserAgent), port(0), size(-1), done(0), connState(CONN_UNKNOWN), coralizeState(aCoralState), socket(NULL), out(tgt) { }

HttpConnection::HttpConnection(string& tgt, CoralizeStates aCoralState, const string& aUserAgent) :
	userAgent(aUserAgent), port(0), size(-1), done(0), connState(CONN_UNKNOWN), coralizeState(aCoralState), socket(NULL) { out.reset(new StringOutputStream(tgt)); }

/**
 * Initiates a file download
 * @todo Abort download
 * @param aFile Fully qualified file URL
 */
void HttpConnection::downloadFile(const string& aFile) {
	currentUrl = aFile;
	prepareRequest(TYPE_GET);
}

/**
 * Initiates a basic urlencoded form submission
 * @param aFile Fully qualified file URL
 * @param aData StringMap with the args and values
 */
void HttpConnection::postData(const string& aFile, const StringMap& aData) {
	currentUrl = aFile;
	coralizeState = CST_NOCORALIZE;
	requestBody.clear();

	for(StringMap::const_iterator i = aData.begin(); i != aData.end(); ++i)
		requestBody += "&" + Util::encodeURI(i->first) + "=" + Util::encodeURI(i->second);

	if (!requestBody.empty()) requestBody = requestBody.substr(1);
	prepareRequest(TYPE_POST);
}

void HttpConnection::abort() {
	abortRequest(true);

	notifyAborted();

	connState = CONN_FAILED;
	coralizeState = CST_DEFAULT;
}

void HttpConnection::abortRequest(bool disconnect) {
	dcassert(socket);

	try {
		done += out->flush();
	} catch(const Exception&) { }

	socket->removeListener(this);
	if(disconnect) socket->disconnect();

	BufferedSocket::putSocket(socket);
	socket = NULL;
}

void HttpConnection::prepareRequest(RequestType type) {
	dcassert(Util::findSubString(currentUrl, "http://") == 0 || Util::findSubString(currentUrl, "https://") == 0);

	// Trim spaces
	while(currentUrl[0] == ' ')
		currentUrl.erase(0, 1);
	while(currentUrl[currentUrl.length() - 1] == ' ') {
		currentUrl.erase(currentUrl.length()-1);
	}

	// Incase this is a retry
	if(done != 0) {
		try {
			out->clear();
		} catch(const Exception& e) {
			notifyFailed(e.getError());
			connState = CONN_FAILED;
			return;
		}
	}

	// Reset the connection states
	if(connState == CONN_OK || connState == CONN_FAILED) 
		userAgent.clear();

	size = -1;
	done = 0;
	connState = CONN_UNKNOWN;
	connType = type;

	// method selection
	method = (connType == TYPE_POST) ? "POST" : "GET";

	// set download type
	if(stricmp(currentUrl.substr(currentUrl.size() - 4), ".bz2") == 0) {
		mimeType = "application/x-bzip2";
	} else mimeType.clear();

	bool isSecure = false;
	string proto, query, fragment;
	if(SETTING(HTTP_PROXY).empty()) {
		Util::decodeUrl(currentUrl, proto, server, port, file, query, fragment, isSecure);
		if(file.empty())
			file = "/";
	} else {
		Util::decodeUrl(SETTING(HTTP_PROXY), proto, server, port, file, query, fragment, isSecure);
		file = currentUrl;
	}

	if(!query.empty())
		file += '?' + query;

	if(BOOLSETTING(CORAL) && coralizeState != CST_NOCORALIZE) {
		if(server.length() > CORAL_SUFFIX.length() && server.compare(server.length() - CORAL_SUFFIX.length(), CORAL_SUFFIX.length(), CORAL_SUFFIX) != 0) {
			server += CORAL_SUFFIX;
		} else {
			coralizeState = CST_NOCORALIZE;
		}
	}

	if(port == 0)
		port = isSecure ? 443 : 80;

	if(userAgent.empty())
		userAgent = APPNAME " rv" BOOST_STRINGIZE(BUILDID);
	
	if(!socket)
		socket = BufferedSocket::getSocket(0x0a);

	socket->addListener(this);

	try {
		socket->connect(server, port, isSecure, true, false);
	} catch(const Exception& e) {
		notifyFailed(e.getError());
		connState = CONN_FAILED;
	} catch(...) { }
}

void HttpConnection::on(BufferedSocketListener::Connected) noexcept { 
	dcassert(socket); 
	socket->write(method + " " + file + " HTTP/1.1\r\n"); 
	socket->write("User-Agent: " + userAgent + "\r\n"); 

	if(connType == TYPE_POST)
	{
		socket->write("Content-Type: application/x-www-form-urlencoded\r\n");
		socket->write("Content-Length: " + Util::toString(requestBody.size()) + "\r\n");
	}

	string sRemoteServer = server; 
	if(!SETTING(HTTP_PROXY).empty()) 
	{
		string tfile, proto, query, fragment;
		uint16_t tport; 
		Util::decodeUrl(file, proto, sRemoteServer, tport, tfile, query, fragment); 
	}

	socket->write("Host: " + sRemoteServer + "\r\n");
	socket->write("Cache-Control: no-cache\r\n"); 
	socket->write("Connection: close\r\n\r\n");	
	if (connType == TYPE_POST) socket->write(requestBody);
	if (coralizeState == CST_DEFAULT) coralizeState = CST_CONNECTED;
} 

void HttpConnection::on(BufferedSocketListener::Line, const string& aLine) noexcept {
	if(connState == CONN_CHUNKED && aLine.size() > 1) {
		string::size_type i;
		string chunkSizeStr;
		if((i = aLine.find(";")) == string::npos) {
			chunkSizeStr = aLine.substr(0, aLine.length() - 1);
		} else chunkSizeStr = aLine.substr(0, i);

		unsigned long chunkSize = strtoul(chunkSizeStr.c_str(), NULL, 16);
		if(chunkSize == 0 || chunkSize == ULONG_MAX) {
			abortRequest(true);

			if(chunkSize == 0) {
				notifyCompleted();
				connState = CONN_OK;
			} else {
				notifyFailed("Chunked transfer failed!");
				connState = CONN_FAILED;
			}

			coralizeState = CST_DEFAULT;
		} else socket->setDataMode(chunkSize);
	} else if(connState == CONN_UNKNOWN) {
		if(aLine.find("200") != string::npos) {
			connState = CONN_OK;
		} else if(aLine.find("301") != string::npos || aLine.find("302") != string::npos) {
			connState = CONN_MOVED; 
		} else {
			abortRequest(true);

			if(SETTING(CORAL) && coralizeState != CST_NOCORALIZE) {		
				coralizeState = CST_NOCORALIZE;
				dcdebug("HTTP error with Coral, retrying : %s\n",currentUrl.c_str());
				downloadFile(currentUrl);
				return;
			}

			notifyFailed(aLine);
			connState = CONN_FAILED;
			coralizeState = CST_DEFAULT;
		}
	} else if(connState == CONN_MOVED && Util::findSubString(aLine, "Location") != string::npos) {
		abortRequest(true);

		string location = aLine.substr(10, aLine.length() - 11);
		// make sure we can also handle redirects with relative paths
		if(location.find("://") == string::npos) {
			if(location[0] == '/') {
				string proto, query, fragment;
				Util::decodeUrl(currentUrl, proto, server, port, file, query, fragment);
				string tmp = proto + "://" + server;
				if(port != 80 && port != 443)
					tmp += ':' + Util::toString(port);
				location = tmp + location;
			} else {
				string::size_type i = currentUrl.rfind('/');
				dcassert(i != string::npos);
				location = currentUrl.substr(0, i + 1) + location;
			}
		}

		if(location == currentUrl) {
			connState = CONN_FAILED;
			coralizeState = CST_DEFAULT;
			notifyFailed((dcpp_fmt("Endless redirection loop (%1%)") % currentUrl).str());
			return;
		}

		notifyRedirected(location);

		if (coralizeState != CST_NOCORALIZE)
			coralizeState = CST_DEFAULT;

		downloadFile(location); 		
	} else if(aLine[0] == 0x0d) {
		if(size != -1) {
			socket->setDataMode(size);
		} else connState = CONN_CHUNKED;
	} else if(Util::findSubString(aLine, "Content-Length") != string::npos) {
		size = Util::toInt(aLine.substr(16, aLine.length() - 17));
		out->setSize(size);
	} else if(mimeType.empty()) { 
		if(Util::findSubString(aLine, "Content-Encoding") != string::npos) {
			if(aLine.substr(18, aLine.length() - 19) == "x-bzip2")
				mimeType = "application/x-bzip2";
		} else if(Util::findSubString(aLine, "Content-Type") != string::npos) {
			mimeType = aLine.substr(14, aLine.length() - 15);
		}
	}
}

void HttpConnection::on(BufferedSocketListener::Failed, const string& aLine) noexcept {
	abortRequest(false);

	if(SETTING(CORAL) && coralizeState != CST_NOCORALIZE) {
		coralizeState = CST_NOCORALIZE;
		dcdebug("Coralized address failed, retrying : %s\n",currentUrl.c_str());
		downloadFile(currentUrl); 
		return;
	}

	connState = CONN_FAILED;
	coralizeState = CST_DEFAULT;
	notifyFailed(aLine);
}

void HttpConnection::on(BufferedSocketListener::ModeChange) noexcept {
	if(connState != CONN_CHUNKED) {
		abortRequest(true);

		notifyCompleted(); 
		coralizeState = CST_DEFAULT;
	}
}

void HttpConnection::on(BufferedSocketListener::Data, uint8_t* aBuf, size_t aLen) noexcept {
	try {
		if(size != -1 && static_cast<size_t>(size - done)  < aLen)
			throw Exception(STRING(TOO_MUCH_DATA));

		out->write(aBuf, aLen);
		done += aLen;
	} catch(const Exception& e) {
		abortRequest(true);

		connState = CONN_FAILED;
		coralizeState = CST_DEFAULT;
		notifyFailed(e.getError());
	}
}

} // namespace dcpp

/**
 * @file
 * $Id: HttpConnection.cpp 1215 2012-01-06 15:59:44Z crise $
 */
